package spacewar2.simulator;

import java.awt.Color;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import spacewar2.actions.DoNothingAction;
import spacewar2.actions.SpacewarAction;
import spacewar2.actions.SpacewarPurchaseEnum;
import spacewar2.clients.Team;
import spacewar2.clients.TeamClient;
import spacewar2.configs.AsteroidConfig;
import spacewar2.configs.BaseConfig;
import spacewar2.configs.HighLevelTeamConfig;
import spacewar2.configs.SpacewarConfig;
import spacewar2.configs.TeamClientConfig;
import spacewar2.gui.SpacewarGUI;
import spacewar2.objects.Asteroid;
import spacewar2.objects.Base;
import spacewar2.objects.Beacon;
import spacewar2.objects.Ship;
import spacewar2.objects.SpacewarObject;
import spacewar2.powerups.SpacewarPowerup;
import spacewar2.shadows.Shadow;
import spacewar2.utilities.Position;
import spacewar2.utilities.Vector2D;

import com.martiansoftware.jsap.JSAPResult;
import com.thoughtworks.xstream.XStream;

public class SpacewarSimulator {

	/**
	 * Max time allowed in MILLISECONDS for a team to return actions
	 */
	public static final int TEAM_ACTION_TIMEOUT = Integer.MAX_VALUE;

	/**
	 * Max time allowed in MILLISECONDS for a getMovement to return
	 */
	public static final int MOVEMENT_TIMEOUT = Integer.MAX_VALUE;

	/**
	 * Max time allowed in MILLISECONDS for a endAction to return
	 */
	public static final int TEAM_END_ACTION_TIMEOUT = Integer.MAX_VALUE;

	/**
	 * Max time allowed in MILLISECONDS for a shadow generation to return
	 */
	public static final int TEAM_SHADOW_TIMEOUT = Integer.MAX_VALUE;



	/**
	 * An array of all the objects in the simulator
	 */
	ArrayList<SpacewarObject> allObjects;

	/**
	 * A list of the clients (e.g. agents who can control ships) in the simulator. It is indexed by team name.
	 */
	HashMap<String,TeamClient> clientMap;

	/**
	 * A list of all teams that can control agents (not indexed)
	 */
	ArrayList<Team> teams;

	/**
	 * If Graphics are turned on, this is a pointer to the main part of the GUI
	 */
	SpacewarGUI gui = null;

	/**
	 * Global random number generator for the game
	 */
	Random random;

	/**
	 * The configuration for this simulation
	 */
	SpacewarConfig simConfig;

	/**
	 * The physics engine
	 */
	Toroidal2DPhysics simulatedSpace;

	/**
	 * Current timestep of the simulator
	 */
	int timestep;

	/**
	 * Create a simulator with the command line arguments already parsed.  
	 * @param args
	 * @throws SimulatorException 
	 */
	public SpacewarSimulator(JSAPResult parserConfig) throws SimulatorException {
		// load in all the configuration
		simConfig = loadConfigFiles(parserConfig);

		allObjects = new ArrayList<SpacewarObject>();
		teams = new ArrayList<Team>();
		clientMap = new HashMap<String, TeamClient>();

		if (simConfig.getRandomSeed() == 0) {
			random = new Random();
		} else {
			random = new Random(simConfig.getRandomSeed());
		}

		// and use it to make agents and the world
		initializeSimulation(parserConfig);

		// create the GUI if the user asked for it
		if (parserConfig.getBoolean("graphics")) {
			gui = new SpacewarGUI(simConfig);

			for (SpacewarObject object : allObjects) {
				if (object.isDrawable()) {
					gui.addShadow(object.getShadow());
				}
			}
		}
	}

	/**
	 * Initialize from an existing config file for the simulator (used by the ladder) 
	 * @param simConfig
	 * @param parserConfig
	 * @throws SimulatorException
	 */
	public SpacewarSimulator(SpacewarConfig simConfig, JSAPResult parserConfig) throws SimulatorException {
		// load in all the configuration
		this.simConfig = simConfig;

		allObjects = new ArrayList<SpacewarObject>();
		teams = new ArrayList<Team>();
		clientMap = new HashMap<String, TeamClient>();

		if (simConfig.getRandomSeed() == 0) {
			random = new Random();
		} else {
			random = new Random(simConfig.getRandomSeed());
		}

		// and use it to make agents and the world
		initializeSimulation(parserConfig);

		// create the GUI if the user asked for it
		if (parserConfig.getBoolean("graphics")) {
			gui = new SpacewarGUI(simConfig);

			for (SpacewarObject object : allObjects) {
				if (object.isDrawable()) {
					gui.addShadow(object.getShadow());
				}
			}
		}
	}


	/**
	 * Sleep so the gui can update (From Andy Fagg's tree code)
	 * @param i
	 */
	static public void mySleep(int i) {
		try{
			Thread.sleep(i);
		}catch(Exception e)
		{
			System.out.println(e);
		}
	}

	/**
	 * Initialize the simulation given a configuration file.  Creates all the objects.
	 * @throws SimulatorException 
	 */
	void initializeSimulation(JSAPResult parserConfig) throws SimulatorException {
		simulatedSpace = new Toroidal2DPhysics(simConfig);

		// place the beacons
		for (int b = 0; b < simConfig.getNumBeacons(); b++) {
			Beacon beacon = new Beacon(simulatedSpace.getRandomFreeLocation(random, Beacon.BEACON_RADIUS * 2), b);
			//System.out.println("New beacon at " + beacon.getPosition());
			simulatedSpace.addObject(beacon);
			allObjects.add(beacon);
		}

		// place the asteroids
		AsteroidConfig asteroidConfig = simConfig.getAsteroids();
		for (int a = 0; a < asteroidConfig.getNumberInitialAsteroids(); a++) {
			double prob = random.nextDouble();
			boolean mineable = false;
			if (prob < asteroidConfig.getProbabilityMineable()) {
				mineable = true;
			}
			int radius = random.nextInt(Asteroid.MAX_ASTEROID_RADIUS - Asteroid.MIN_ASTEROID_RADIUS) + Asteroid.MIN_ASTEROID_RADIUS;

			prob = random.nextDouble();
			boolean moveable = false;
			if (prob < asteroidConfig.getProbabilityMoveable()) {
				moveable = true;
			}

			Asteroid asteroid = new Asteroid(simulatedSpace.getRandomFreeLocation(random, radius * 2), mineable, radius, moveable);
			simulatedSpace.addObject(asteroid);
			allObjects.add(asteroid);

			if (asteroid.isMoveable()) {
				Vector2D randomMotion = Vector2D.getRandom(random, asteroidConfig.getMaxInitialVelocity());
				asteroid.getPosition().setTranslationalVelocity(randomMotion);
			}

		}

		// create the clients
		for (HighLevelTeamConfig teamConfig : simConfig.getTeams()) {
			// ensure this team isn't a duplicate
			if (clientMap.containsKey(teamConfig.getTeamName())) {
				throw new SimulatorException("Error: duplicate team name " + teamConfig.getTeamName());
			}

			TeamClientConfig teamClientConfig = getTeamClientConfig(teamConfig, parserConfig.getString("configPath"));

			// if the team doesn't provide default radiii and bases, create one
			if (teamConfig.getInitialRegionRadius() == 0) {
				teamConfig.setInitialRegionRadius(simulatedSpace.width / 4);
				teamConfig.setInitialRegionCenterX(random.nextInt(simConfig.getWidth()));
				teamConfig.setInitialRegionCenterY(random.nextInt(simConfig.getHeight()));
				System.out.println("Base location not provided for team " + teamConfig.getTeamName() + "...generating: (X, Y, radius) = " + 
						teamConfig.getInitialRegionCenterX() + "," + teamConfig.getInitialRegionCenterY() + "," + teamConfig.getInitialRegionRadius());
			}


			TeamClient teamClient = createTeamClient(teamConfig, teamClientConfig);

			// make the team inside the simulator for this team
			Team team = createTeam(teamConfig, teamClient, teamClientConfig);

			for (Ship ship : team.getShips()) {
				allObjects.add(ship);
				simulatedSpace.addObject(ship);
			}

			clientMap.put(teamConfig.getTeamName(), teamClient);
		}

		// make sure the base count matches the team count
		if (simConfig.getTeams().length != simConfig.getBases().length) {
			throw new SimulatorException("Error: You specified " + simConfig.getTeams().length + 
					" teams and " + simConfig.getBases().length + " bases.  They must match.");
		}

		// create the bases and ensure there is a base for each team
		for (BaseConfig baseConfig : simConfig.getBases()) {
			String teamName = baseConfig.getTeamName();
			if (!clientMap.containsKey(teamName)) {
				throw new SimulatorException("Error: base is listed as team " + teamName + " but there is no corresponding team");
			} 

			TeamClient teamClient = clientMap.get(teamName);

			// find the team config for this team
			HighLevelTeamConfig thisTeamConfig = null;
			for (HighLevelTeamConfig teamConfig : simConfig.getTeams()) {
				if (teamConfig.getTeamName().equalsIgnoreCase(teamName)) {
					thisTeamConfig = teamConfig;
					break;
				}

			}

			// make the base in the region specified for this team
			Position freeLocation = simulatedSpace.getRandomFreeLocationInRegion(random, Base.BASE_RADIUS, 
					thisTeamConfig.getInitialRegionCenterX(), thisTeamConfig.getInitialRegionCenterY(),
					thisTeamConfig.getInitialRegionRadius());


			// get this team as well as the client
			Team thisTeam = null;
			for (Team team : teams) {
				if (team.getTeamName().equalsIgnoreCase(teamName)) {
					thisTeam = team;
					break;
				}
			}

			Base base = new Base(freeLocation, baseConfig.getTeamName(), thisTeam, true);
			allObjects.add(base);
			simulatedSpace.addObject(base);
			thisTeam.addBase(base);
		}

	}

	/**
	 * Make the actual team (holds a pointer to the client) 
	 * 
	 * @param teamConfig
	 * @param teamClient
	 * @return
	 */
	public Team createTeam(HighLevelTeamConfig teamConfig, TeamClient teamClient, TeamClientConfig teamClientConfig) {
		// it succeeded!  Now make the team ships
		int numShips = Math.min(simConfig.getMaximumShipsPerTeam(), teamClientConfig.getNumberInitialShipsInTeam());

		Team team = new Team(teamClient, teamClientConfig.getLadderName());

		for (int s = 0; s < numShips; s++) {
			// put the ships in the initial region for the team
			Position freeLocation = simulatedSpace.getRandomFreeLocationInRegion(random, Ship.SHIP_RADIUS, 
					teamConfig.getInitialRegionCenterX(), teamConfig.getInitialRegionCenterY(),
					teamConfig.getInitialRegionRadius());
			Ship ship = new Ship(teamConfig.getTeamName(), team.getTeamColor(), freeLocation);
			team.addShip(ship);
		}

		teams.add(team);
		return team;
	}


	/**
	 * Make the team client from the configuration file
	 * 
	 * @param teamConfig
	 * @return
	 * @throws SimulatorException 
	 * @throws ClassNotFoundException 
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 */
	public TeamClientConfig getTeamClientConfig(HighLevelTeamConfig teamConfig, String configPath) throws SimulatorException {
		String fileName = configPath + teamConfig.getConfigFile();

		XStream xstream = new XStream();
		xstream.alias("TeamClientConfig", TeamClientConfig.class);
		TeamClientConfig lowLevelTeamConfig;

		try { 
			lowLevelTeamConfig = (TeamClientConfig) xstream.fromXML(new File(fileName));
		} catch (Exception e) {
			throw new SimulatorException("Error parsing config team config file " + fileName + " at string " + e.getMessage());
		}

		return lowLevelTeamConfig;
	}



	/**
	 * Make the team client from the configuration file
	 * 
	 * @param teamConfig
	 * @return
	 * @throws SimulatorException 
	 */
	@SuppressWarnings("unchecked")
	public TeamClient createTeamClient(HighLevelTeamConfig teamConfig, TeamClientConfig teamClientConfig) throws SimulatorException {
		try {
			// make a team client of the class specified in the config file
			Class<TeamClient> newTeamClass = (Class<TeamClient>) Class.forName(teamClientConfig.getClassname());
			TeamClient newTeamClient = (TeamClient) newTeamClass.newInstance();

			Color teamColor = new Color(teamClientConfig.getTeamColorRed(), teamClientConfig.getTeamColorGreen(), 
					teamClientConfig.getTeamColorBlue());
			newTeamClient.setTeamColor(teamColor);
			newTeamClient.setTeamName(teamConfig.getTeamName());
			newTeamClient.setKnowledgeFile(teamClientConfig.getKnowledgeFile());
			newTeamClient.setRandom(random);
			newTeamClient.initialize();
			return newTeamClient;
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
			throw new SimulatorException("Unable to make a new team client " + teamClientConfig.getClassname());
		} catch (InstantiationException e) {
			e.printStackTrace();
			throw new SimulatorException("Unable to create a new instance of class " + teamClientConfig.getClassname());
		} catch (IllegalAccessException e) {
			e.printStackTrace();
			throw new SimulatorException("Unable to create a new instance of class " + teamClientConfig.getClassname());
		}

	}

	/**
	 * @return all objects in the simulator
	 */
	public ArrayList<SpacewarObject> getAllObjects() {
		return allObjects;
	}

	/**
	 * Load in the configuration files
	 * @throws SimulatorException 
	 * 
	 */
	public SpacewarConfig loadConfigFiles(JSAPResult parserConfig) throws SimulatorException {
		String configFile = parserConfig.getString("configPath") + parserConfig.getString("simulatorConfigFile");

		XStream xstream = new XStream();
		xstream.alias("SpacewarConfig", SpacewarConfig.class);
		xstream.alias("HighLevelTeamConfig", HighLevelTeamConfig.class);
		xstream.alias("BaseConfig", BaseConfig.class);
		xstream.alias("AsteroidConfig", AsteroidConfig.class);

		try { 
			simConfig = (SpacewarConfig) xstream.fromXML(new File(configFile));
		} catch (Exception e) {
			throw new SimulatorException("Error parsing config file at string " + e.getMessage());
		}
		return simConfig;
	}

	/**
	 * Advance time one step
	 */
	void advanceTime() {
		ExecutorService teamExecutor = Executors.newCachedThreadPool();
		Map<Team, Future<Map<UUID,SpacewarAction>>> clientActionFutures = 
				new HashMap<Team, Future<Map<UUID,SpacewarAction>>>();

		// get the actions from each team
		for (Team team : teams) {
			clientActionFutures.put(team, teamExecutor.submit(new AdvanceTimeCallable(team)));
		}

		for (Team team : teams) {
			Map<UUID, SpacewarAction> teamActions;

			try {
				teamActions = clientActionFutures.get(team).get();
			} catch (InterruptedException e) {
				//something went wrong...return empty map
				teamActions = new HashMap<UUID, SpacewarAction>();
			} catch (ExecutionException e) {
				//something went wrong...return empty map
				teamActions = new HashMap<UUID, SpacewarAction>();
			}


			// get the actions for each ship
			for (Ship ship : team.getShips()) {
				// if the client forgets to set an action, set it to DoNothing
				if (!teamActions.containsKey(ship.getId())) {
					teamActions.put(ship.getId(), new DoNothingAction());
				}
				ship.setCurrentAction(teamActions.get(ship.getId()));
			}

			// update the GUI if needed
			if (gui != null) {
				Set<Shadow> newShadows = team.getNewShadows();
				if (newShadows != null) {
					for (Shadow shadow : newShadows) {
						gui.addShadow(shadow);
					}
				}
			}


			// now get purchases for the team
			Map<UUID, SpacewarPurchaseEnum> purchases = team.getTeamPurchases(simulatedSpace);
			handlePurchases(team, purchases);
		}

		teamExecutor.shutdown();


		// get any new shadows for the gui
		if (gui != null) {
			ArrayList<Shadow> newShadows = simulatedSpace.getNewShadows();
			if (newShadows != null) {
				for (Shadow shadow : newShadows) {
					gui.addShadow(shadow);
				}
			}
			simulatedSpace.resetNewShadows();
		}

		// get the power ups being used on this turn
		Map<UUID, SpacewarPowerup> allPowerups = new HashMap<UUID, SpacewarPowerup>();
		for (Team team : teams) {
			Map<UUID, SpacewarPowerup> powerups = team.getTeamPowerups(simulatedSpace);
			if (powerups != null) {
				for (UUID key : powerups.keySet()) {
					allPowerups.put(key, powerups.get(key));
				}
			}
		}

		// now update the physics on all objects
		simulatedSpace.advanceTime(this.getTimestep(), allPowerups);

		// and end any actions inside the team
		for (Team team : teams) {
			team.getClientMovementEnd(simulatedSpace);

			if (gui != null) {
				Set<Shadow> oldShadows = team.getOldShadows();
				if (oldShadows != null) {
					for (Shadow shadow : oldShadows) {
						gui.removeShadow(shadow);
					}
				}
			}
		}


		// cleanup and remove dead weapons
		simulatedSpace.cleanupDeadWeapons();

		if (gui != null) {
			ArrayList<Shadow> oldShadows = simulatedSpace.getOldShadows();
			if (oldShadows != null) {
				for (Shadow shadow : oldShadows) {
					gui.removeShadow(shadow);
				}
			}
			simulatedSpace.resetOldShadows();
		}

		// respawn any objects that died (and that should respawn)
		simulatedSpace.respawnDeadObjects(random);

		// TODO: spawn new asteroids with a small probability

		updateScores();
	}

	/**
	 * Handle purchases for a team
	 * NOTE: Only BASE purchases work at the moment
	 * TODO: implement the rest of purchasing
	 * 
	 * @param team
	 * @param purchases
	 */
	private void handlePurchases(Team team,	Map<UUID, SpacewarPurchaseEnum> purchases) {
		// handle teams that don't purchase
		if (purchases == null) {
			return;
		}
		
		for (UUID key : purchases.keySet()) {
			SpacewarPurchaseEnum purchase = purchases.get(key);

			switch (purchase) {
			case BASE:
				// only do it if there is enough money (otherwise ignore)
				if (team.getAvailableMoney() >= team.getCurrentCostNewBase()) {
//					System.out.println("Implementing base buying");
					// set the base just away from the ship (to avoid a collision)
					Ship ship = (Ship) simulatedSpace.getObjectById(key);
					Position newPosition = simulatedSpace.getRandomFreeLocationInRegion(random, 
							Base.BASE_RADIUS, (int) ship.getPosition().getX(), 
							(int) ship.getPosition().getY(), (2 * ship.getRadius() + Base.BASE_RADIUS));
//					double x = ship.getPosition().getX() + ship.getRadius() + Base.BASE_RADIUS;
//					double y = ship.getPosition().getY() + ship.getRadius() + Base.BASE_RADIUS;
//					Position newPosition = new Position(x, y);

					// only create/buy the base if the goal location is free
					if (simulatedSpace.isLocationFree(newPosition, Base.BASE_RADIUS)) {
						// make the new base and add it to the lists
						Base base = new Base(newPosition, team.getTeamName(), team, false);
						allObjects.add(base);
						simulatedSpace.addObject(base);
						team.addBase(base);
						if (gui != null) {
							gui.addShadow(base.getShadow());
						}

						// charge the team for the base and double the charge for a new base
//						System.out.println("Available money before " + team.getAvailableMoney());
						team.incrementAvailableMoney(-team.getCurrentCostNewBase());
						team.setCurrentCostNewBase(team.getCurrentCostNewBase() * 2);
//						System.out.println("Available money after " + team.getAvailableMoney());
					}
				}
				break;
			case POWERUP_SHIELD:
				break;
			case NOTHING:
				break;
			default:
				break;
			}

		}

	}

	/**
	 * Updates the scores for the teams
	 */
	private void updateScores() {
		if (simConfig.getScoringMethod().equalsIgnoreCase("Money")) {
			for (Team team : teams) {
				team.setScore(team.getTotalMoney());
			}
		} else if (simConfig.getScoringMethod().equalsIgnoreCase("Beacons")) {
			for (Team team : teams) {
				int beacons = 0;
				for (Ship ship : team.getShips()) {
					beacons += ship.getNumBeacons();
				}
				team.setScore(beacons);
			}
		} else {
			System.err.println("Error: Scoring method " + simConfig.getScoringMethod() + " is not recognized.  Scores will all be 0.");
		}

	}

	/**
	 * Main control loop of the simulator
	 * @throws SimulatorException
	 */
	public void run() throws SimulatorException {
		if (gui != null) {
			gui.redraw(this);
		}

		// run the game loop until the maximum time has elapsed
		for (timestep = 0; timestep < simConfig.getSimulationSteps(); timestep++) {
			advanceTime();

			if (gui != null) {
				gui.redraw(this);
				mySleep(40);
			}

			if (timestep % 5000 == 0) {
				System.out.println("On time step " + timestep);
			}
		}

		shutdownTeams();
	}

	/**
	 * Called after the simulation ends so the clients all cleanly shutdown
	 */
	public void shutdownTeams() {
		for (Team team : teams) {
			team.shutdownClients();
		}
	}

	/**
	 * Returns the current timestep
	 * @return
	 */
	public int getTimestep() {
		return timestep;
	}

	/**
	 * Returns the list of teams
	 * @return
	 */
	public ArrayList<Team> getTeams() {
		return teams;
	}

	/**
	 * Inner class to do parallel actions from clients
	 * @author amy
	 *
	 */
	class AdvanceTimeCallable implements  Callable<Map<UUID,SpacewarAction>>{
		private Team team;

		AdvanceTimeCallable(Team team){
			this.team = team;
		}

		public Map<UUID,SpacewarAction> call() throws Exception {
			if(this.team != null){
				return this.team.getClientMovementStart(simulatedSpace);
			}else{
				//something went wrong...lets return empty map
				return new HashMap<UUID, SpacewarAction>();
			}

		}
	}
}
